package com.laviedesfons.jhmtasc.metadata;

import com.laviedesfons.jhmtasc.annotations.binding.*;
import com.laviedesfons.jhmtasc.types.internals.PVoid;
import java.util.HashMap;
import java.util.Map;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.SimpleElementVisitor6;
import javax.lang.model.util.SimpleTypeVisitor6;

/**
 *
 * @author Romano
 */
public class MetaTypeVisitor extends SimpleTypeVisitor6<MetaType, Object> {

    static Map<String, MetaType> types = new HashMap<String, MetaType>();
    SimpleElementVisitor6<MetaModel, MetaModel> metaModelVisitor = new MetaModelVisitor(this);

    class MetaModelVisitor extends SimpleElementVisitor6<MetaModel, MetaModel> {

        MetaTypeVisitor metaTypeVisitor;
        MetaModelVisitor(MetaTypeVisitor mt){
            metaTypeVisitor = mt;
        }

        @Override
        public MetaModel visitVariable(VariableElement ve, MetaModel p) {
            return ve.accept(metaModelVisitor, null);
        }

        @Override
        public MetaModel visitType(TypeElement te, MetaModel m) {

            if (m.getParams() == null) {
                // look for alias
                MetaType parent = te.getSuperclass().accept(metaTypeVisitor, m);
                if (parent != null) {
                    m.setParams(parent.getParams());
                    m.setIsAlias(true);
                }
            }

            int offsetCounter = 0;
            int valueCounter = 0;
            for (Element elt :
                    ElementFilter.fieldsIn(te.getEnclosedElements())) {

                MFlaggedField flaggedFieldAnnot =
                        elt.getAnnotation(MFlaggedField.class);
                MField fieldAnnot = elt.getAnnotation(MField.class);

                if (flaggedFieldAnnot == null && fieldAnnot == null) {
                    continue;
                }

                MetaField field = new MetaField((MetaModel) m);
                if (flaggedFieldAnnot != null) {
                    field.setFlag(flaggedFieldAnnot.flag());
                }

                field.setFieldName(elt.getSimpleName().toString().toUpperCase());
                field.setMetaType(elt.asType().accept(metaTypeVisitor, m));

                if (elt.asType().toString().equals(PVoid.class.getName())) {
                    field.setId(offsetCounter);
                    field.setKind(MetaField.Kind.OFFSET);
                    ((MetaModel) m).addOffsetField(field);
                    offsetCounter++;
                } else {
                    field.setId(valueCounter);
                    field.setKind(MetaField.Kind.VALUE);
                    ((MetaModel) m).addValueField(field);
                    valueCounter++;
                }
            }
            return m;
        }
    }

    @Override
    public MetaType visitDeclared(DeclaredType dt, Object p) {

        String name = dt.asElement().asType().toString();
        MetaType m = types.get(name);
        if (m != null) {
            return m;
        }

        MRootType mRootType = dt.asElement().getAnnotation(MRootType.class);
        MType mType = dt.asElement().getAnnotation(MType.class);
        MFlaggedType mFlaggedType = dt.asElement().getAnnotation(MFlaggedType.class);

        boolean aliasing = false;

        if (mType != null) {
            m = new MetaType(name);
            if (!mType.camlType().equals("[unassigned]")) {
                m.setCamlType(mType.camlType());
            }

        // TODO enforce implements InternalType here..
        } else if ((mRootType != null && mFlaggedType == null)
                || (mFlaggedType != null && mRootType == null)) {

            m = new MetaModel(name);
            m.setIsPrimitive(false);

            // TODO enforce extends AbstractType here!

            if (mRootType != null) {
                m.setCamlType(mRootType.camlType());
                ((MetaModel) m).setIsRootType(true);
            } else {
                m.setCamlType(mFlaggedType.camlType());
                ((MetaModel) m).setFlag(mFlaggedType.flag());
            }
        } else {
            // aliasing some generic parent
            // this pass is meant to gather the generic params
            if (name.endsWith("WrappedType<T>")) {
                m = new MetaType(name);
                aliasing = true;
            } else {
                return null;
            }
        }

        if(!aliasing){
            types.put(dt.toString(), m);
        }

        MetaType[] pms = null;
        if (!dt.getTypeArguments().isEmpty()) {
            pms = new MetaType[dt.getTypeArguments().size()];
            int i = 0;
            for (TypeMirror tm : dt.getTypeArguments()) {
                pms[i] = tm.accept(this, null);
                i++;
            }
        }

        if(pms != null){
            m.setParams(pms);
        }

        if(!m.isPrimitive()){
            dt.asElement().accept(metaModelVisitor, (MetaModel)m);
        }

        return m;
    }
}
